﻿#region The MIT License
// The MIT License
// 
// Copyright (c) 2009 octalforty studios
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
#endregion
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;

using octalforty.Chinchillin.Actions.Sync;
using octalforty.Chinchillin.Shared.Deployment;
using octalforty.Chinchillin.Shared.Deployment.Metadata;
using octalforty.Chinchillin.Shared.Deployment.Resources;
using octalforty.Chinchillin.Shared.Deployment.Resources.Impl;

namespace octalforty.Chinchillin.Actions.Fs
{
    [Action(Name)]
    public class SyncDirectoryAction : ActionBase, ISyncCallback<string, string>, IEqualityComparer<SyncItem<string, string>>
    {
        #region Private Constants
        private const string Name = "sync-directory";
        private const string SourceLocation = "source-location";
        private const string TargetLocation = "target-location";

        private static readonly ActionInfo actionInfo = new ActionInfo
            {
                Name = Name,
                Arguments = new[]
                    {
                        new ActionArgumentInfo(SourceLocation, ActionArgumentType.Uri),
                        new ActionArgumentInfo(TargetLocation, ActionArgumentType.Uri),
                    }
            };

        private static string[] ignores = new[] { ".svn" };
        #endregion

        #region Private Types
        private class SyncContext
        {
            public Uri SourceBase
            { get; private set; }

            public Uri TargetBase
            { get; private set; }

            public IActionExecutionContext ActionExecutionContext
            { get; private set; }

            public SyncContext(Uri sourceBase, Uri targetBase, IActionExecutionContext actionExecutionContext)
            {
                SourceBase = sourceBase;
                TargetBase = targetBase;
                ActionExecutionContext = actionExecutionContext;
            }
        }
        #endregion

        #region Public Properties
        public IResourceProvider LocalResourceProvider
        { get; set; }

        public IFileSystemManager FileSystemManager
        { get; set; }
        #endregion

        public SyncDirectoryAction() :
            base(actionInfo)
        {
        }

        public override void Execute(IActionExecutionContext executionContext)
        {
            try
            {
                var sourceLocation = (Uri)executionContext.Arguments[SourceLocation];
                var targetLocation = (Uri)executionContext.Arguments[TargetLocation];

                var sourceResources = Expand(sourceLocation, DeploymentCallback.GetResourcesHierarchy(sourceLocation));
                var targetResources = Expand(targetLocation, ChangeScheme(LocalResourceProvider.GetResourcesHierarchy(targetLocation), targetLocation.Scheme));

                TracingManager.TraceEvent(executionContext, TraceEventType.Information, 0, 
                    string.Format("Synchronizing {0} items on '{1}' with {2} items on '{3}'", 
                        sourceResources.Length, sourceLocation, targetResources.Length, targetLocation));

                var sourceResourcesChecksums = DeploymentCallback.GetResourcesChecksums(sourceResources).ToDictionary(ri => ri.Uri, ri => ri.Checksum);

                var sourceItems = ToSyncItemList(sourceLocation, sourceResources,
                    u => Convert.ToBase64String(sourceResourcesChecksums[u]));

                var targetItems = ToSyncItemList(targetLocation, targetResources,
                    u => Convert.ToBase64String(LocalResourceProvider.GetResourceChecksum(u)));

                var syncManager = new SyncManager<string, string>(this, new SyncContext(sourceLocation, targetLocation, executionContext), this);
                var syncResult = syncManager.SyncOneWay(sourceItems, targetItems);

                TracingManager.TraceEvent(executionContext, TraceEventType.Information, 0, 
                    string.Format("Completed synchronizing {0} entries on '{1}' with {2} entries on '{3}'. {4} items skipped, {5} items deleted, {6} items synced.", 
                        sourceResources.Length, sourceLocation, targetResources.Length, targetLocation,
                        syncResult.Skipped, syncResult.Deleted, syncResult.Synced));
            }
            catch(Exception e)
            {
                TracingManager.TraceEvent(executionContext, TraceEventType.Error, 0, e.ToString());
                throw;
            }
        }

        private IEnumerable<Uri> ChangeScheme(IEnumerable<Uri> hierarchy, string scheme)
        {
            foreach(var uri in hierarchy)
            {
                var b = new UriBuilder(uri);
                b.Scheme = scheme;

                yield return b.Uri;
            } // foreach
        }

        private static List<SyncItem<string, string>> ToSyncItemList(Uri location, IEnumerable<Uri> resources, Func<Uri, string> checksumProvider)
        { 
            var syncItems = new List<SyncItem<string, string>>(
                resources.
                    Select(u => new SyncItem<string, string>(location.MakeRelativeUri(u).ToString(), () => checksumProvider(u))).
                    Where(si => !ignores.Any(i => si.Identity.Contains(i))));
            return syncItems;
        }

        private Uri[] Expand(Uri baseUri, IEnumerable<Uri> uris)
        {
            /*List<Uri> us = new List<Uri>(uris);

            foreach(var u in uris)
            {
                var _u = u;

                while(_u != baseUri)
                {
                    _u = _u.
                    if(!us.Contains(_u))
                        us.Add(_u);
                } // while
            } // foreach
*/
            return uris.ToArray();
        }

        #region ISyncCallback<string, string> Members
        public void SkipLocalItem(ISyncContext context, SyncItem<string, string> item)
        {
            var sc = (SyncContext)context.Context;

            var uri = new Uri(sc.TargetBase, item.Identity);
            TracingManager.TraceEvent(sc.ActionExecutionContext, TraceEventType.Verbose, 0,
                string.Format("Skipping '{0}'", uri));
        }

        public void DeleteLocalItem(ISyncContext context, SyncItem<string, string> item)
        {
            var sc = (SyncContext)context.Context;

            var uri = new Uri(sc.TargetBase, item.Identity);
            TracingManager.TraceEvent(sc.ActionExecutionContext, TraceEventType.Verbose, 0,
                string.Format("Deleting '{0}'", uri));

            File.Delete(uri.LocalPath);
        }

        public void SyncRemoteItem(ISyncContext context, SyncItem<string, string> item)
        {
            var sc = (SyncContext)context.Context;

            var targetUri = new Uri(sc.TargetBase, item.Identity);
            var sourceUri = new Uri(sc.SourceBase, item.Identity);
            var path = targetUri.LocalPath;

            TracingManager.TraceEvent(sc.ActionExecutionContext, TraceEventType.Verbose, 0,
                string.Format("Syncing '{0}'", targetUri));

            if(!Directory.Exists(Path.GetDirectoryName(path)))
                Directory.CreateDirectory(Path.GetDirectoryName(path));

            using(var sourceStream = DeploymentCallback.GetResourceStream(sourceUri))
            {
                using(var targetStream = FileSystemManager.CreateFile(path))
                {
                    FileSystemManager.CopyStream(sourceStream, targetStream);
                } // using
            } // using

        }
        #endregion

        #region IEqualityComparer<SyncItem<string, string>> Members
        public bool Equals(SyncItem<string, string> x, SyncItem<string, string> y)
        {
            return x.Equals(y);
        }

        public int GetHashCode(SyncItem<string, string> obj)
        {
            unchecked
            {
                return (obj.Identity.ToLowerInvariant().GetHashCode() * 397) ^ obj.Checksum.GetHashCode();
            }
        }
        #endregion
    }
}
